"""
Type mapper for converting C# types to C++ equivalents.

This module provides the TypeMapper class which handles type conversion including:
- Primitive types
- Generic types (List<T>, Dictionary<K,V>, etc.)
- Nullable reference types (string? -> std::optional<std::string>)
- Arrays (T[] -> std::vector<T>)
"""

from __future__ import annotations

import re
from pathlib import Path
from typing import Any, List

import yaml


class TypeMappingResult:
    """Result of a type mapping operation"""

    def __init__(
        self,
        cpp_type: str,
        includes: list[str],
        comment: str | None = None,
        is_user_defined: bool = False,
    ):
        self.cpp_type = cpp_type
        self.includes = includes
        self.comment = comment
        self.is_user_defined = is_user_defined

    def __repr__(self) -> str:
        return f"TypeMappingResult(cpp_type={self.cpp_type!r}, includes={self.includes!r})"


class TypeMapper:
    """
    Maps C# types to C++ equivalents.

    Handles primitives, generics, nullables, arrays, and custom mappings.
    """

    def __init__(self, config_path: str | Path | None = None):
        """
        Initialize TypeMapper with optional config path.

        Args:
            config_path: Path to type-mappings.yaml. If None, uses default config.
        """
        if config_path is None:
            # Default to config/type-mappings.yaml relative to project root
            default_path = Path(__file__).parent.parent.parent.parent / "config" / "type-mappings.yaml"
            config_path = default_path

        self.config_path = Path(config_path)
        self.mappings: dict[str, Any] = {}
        self._load_config()

    def _load_config(self) -> None:
        """Load type mappings from YAML configuration"""
        if not self.config_path.exists():
            raise FileNotFoundError(f"Type mappings config not found: {self.config_path}")

        with open(self.config_path, "r") as f:
            self.mappings = yaml.safe_load(f)

    def map_type(self, csharp_type: str, use_const_ref: bool = False) -> TypeMappingResult:
        """
        Map a C# type to its C++ equivalent.

        Args:
            csharp_type: C# type name (e.g., "int", "List<string>", "string?", "int[]")
            use_const_ref: If True, wrap non-primitive types in const reference

        Returns:
            TypeMappingResult with C++ type and required includes

        Examples:
            >>> mapper.map_type("int")
            TypeMappingResult(cpp_type='int32_t', includes=['<cstdint>'])

            >>> mapper.map_type("List<string>")
            TypeMappingResult(cpp_type='std::vector<std::string>', includes=['<vector>', '<string>'])

            >>> mapper.map_type("string?")
            TypeMappingResult(cpp_type='std::optional<std::string>', includes=['<optional>', '<string>'])
        """
        # Remove whitespace and normalize malformed generics
        csharp_type = csharp_type.strip()
        csharp_type = self._fix_incomplete_generics(csharp_type)
        csharp_type = self._fix_incomplete_generics(csharp_type)

        # Handle nullable types (e.g., "string?", "int?")
        if csharp_type.endswith("?"):
            return self._map_nullable_type(csharp_type[:-1])

        # Handle array types (e.g., "int[]", "string[]")
        if csharp_type.endswith("[]"):
            return self._map_array_type(csharp_type[:-2])

        # Handle generic types (e.g., "List<T>", "Dictionary<K,V>")
        if "<" in csharp_type and ">" in csharp_type:
            return self._map_generic_type(csharp_type)

        # Handle primitive and simple types
        return self._map_simple_type(csharp_type, use_const_ref)

    def _fix_incomplete_generics(self, type_str: str) -> str:
        """
        Normalize malformed generic type strings before processing.

        Fixes cases such as:
            - Missing closing brackets (Func<T, -> Func<T>)
            - Trailing commas or excess whitespace
            - Lowercase single-letter generic parameters (Func<T, t> -> Func<T, T>)
        """
        type_str = type_str.strip()
        if "<" not in type_str:
            return type_str

        # Remove trailing commas that break parsing (Func<T, -> Func<T>)
        type_str = re.sub(r",\s*$", "", type_str)

        # Tidy whitespace inside the generic signature to help parsing
        type_str = re.sub(r"<\s+", "<", type_str)
        type_str = re.sub(r"\s+>", ">", type_str)
        type_str = re.sub(r"\s*,\s*", ", ", type_str)

        # Balance angle brackets by appending missing closing tokens
        open_count = type_str.count("<")
        close_count = type_str.count(">")
        if open_count > close_count:
            type_str = f"{type_str}{'>' * (open_count - close_count)}"
        elif close_count > open_count:
            # Trim surplus closing brackets from the end
            to_trim = close_count - open_count
            while to_trim > 0 and type_str.endswith(">"):
                type_str = type_str[:-1]
                to_trim -= 1

        # Upper-case single-letter generic parameters (Func<T, t> -> Func<T, T>)
        def _upper_single_letter(match: re.Match[str]) -> str:
            prefix = match.group(1)
            letter = match.group(2)
            return f"{prefix}{letter.upper()}"

        type_str = re.sub(r"(<|,\s*)([a-z])(?=\s*(?:,|>))", _upper_single_letter, type_str)

        return type_str

    def _map_simple_type(self, csharp_type: str, use_const_ref: bool = False) -> TypeMappingResult:
        """Map a simple (non-generic, non-nullable) type"""
        # Check user overrides first
        user_overrides = self.mappings.get("user_overrides", {})
        if user_overrides and csharp_type in user_overrides:
            mapping = user_overrides[csharp_type]
            return TypeMappingResult(
                cpp_type=mapping["cpp_type"],
                includes=mapping.get("includes", []),
                comment=mapping.get("comment"),
                is_user_defined=True,
            )

        # Check primitives
        primitives = self.mappings.get("primitives", {})
        if primitives and csharp_type in primitives:
            mapping = primitives[csharp_type]
            return TypeMappingResult(
                cpp_type=mapping["cpp_type"],
                includes=mapping.get("includes", []),
                comment=mapping.get("comment"),
            )

        # Check strings
        strings = self.mappings.get("strings", {})
        if strings and csharp_type in strings:
            mapping = strings[csharp_type]
            cpp_type = mapping["cpp_type"]
            if use_const_ref:
                cpp_type = f"const {cpp_type}&"
            return TypeMappingResult(
                cpp_type=cpp_type,
                includes=mapping.get("includes", []),
                comment=mapping.get("comment"),
            )

        # Check system types
        system_types = self.mappings.get("system_types", {})
        if system_types and csharp_type in system_types:
            mapping = system_types[csharp_type]
            return TypeMappingResult(
                cpp_type=mapping["cpp_type"],
                includes=mapping.get("includes", []),
                comment=mapping.get("comment"),
            )

        # If not found, assume it's a user-defined type (class name)
        # Keep the same name for user-defined types
        return TypeMappingResult(
            cpp_type=csharp_type,
            includes=[],
            is_user_defined=True,
        )

    def _map_nullable_type(self, inner_type: str) -> TypeMappingResult:
        """Map nullable type (e.g., "string?" -> "std::optional<std::string>")"""
        # First map the inner type
        inner_result = self.map_type(inner_type)

        # Wrap in std::optional
        nullable_config = self.mappings.get("nullable", {})
        cpp_wrapper = nullable_config.get("cpp_wrapper", "std::optional<{T}>")
        cpp_type = cpp_wrapper.replace("{T}", inner_result.cpp_type)

        # Combine includes
        includes = list(inner_result.includes)
        includes.extend(nullable_config.get("includes", []))

        return TypeMappingResult(
            cpp_type=cpp_type,
            includes=includes,
            comment=f"Nullable type from {inner_type}?",
        )

    def _map_array_type(self, element_type: str) -> TypeMappingResult:
        """Map array type (e.g., "int[]" -> "std::vector<int32_t>")"""
        # First map the element type
        element_result = self.map_type(element_type)

        # Use std::vector for arrays
        cpp_type = f"std::vector<{element_result.cpp_type}>"

        # Combine includes
        includes = ["<vector>"]
        includes.extend(element_result.includes)

        return TypeMappingResult(
            cpp_type=cpp_type,
            includes=includes,
            comment=f"Array type from {element_type}[]",
        )

    def _map_generic_type(self, csharp_type: str) -> TypeMappingResult:
        """
        Map generic type (e.g., "List<string>" -> "std::vector<std::string>")

        Handles:
        - List<T>
        - Dictionary<K,V>
        - Other generic collections
        """
        # Parse generic type
        match = re.match(r"^([^<]+)<(.+)>$", csharp_type)
        if not match:
            # Fallback to simple type if parsing fails
            return self._map_simple_type(csharp_type)

        generic_name = match.group(1).strip()
        type_args_str = match.group(2).strip()

        # Parse type arguments (handles nested generics)
        type_args = self._parse_type_arguments(type_args_str)

        # Map each type argument
        mapped_args = [self.map_type(arg) for arg in type_args]

        # Handle delegate-style generics (Func<>, Action<>, etc.)
        delegate_result = self._map_delegate_type(generic_name, mapped_args)
        if delegate_result is not None:
            return delegate_result

        # Attempt to repair truncated dictionary generics like Dictionary<string,>
        if generic_name in {"Dictionary", "IDictionary"} and len(mapped_args) == 1:
            type_args.append(type_args[0])
            mapped_args.append(mapped_args[0])

        # Handle delegate types explicitly
        if generic_name in {"Func", "Action", "ActionRef", "FuncRef"}:
            return self._map_delegate_type(generic_name, type_args, mapped_args)

        # Look up the generic type mapping
        collections = self.mappings.get("collections", {})

        # Build pattern key (e.g., "List<T>", "Dictionary<K,V>")
        if len(type_args) == 1:
            pattern_key = f"{generic_name}<T>"
        elif len(type_args) == 2:
            pattern_key = f"{generic_name}<K,V>"
        else:
            # Handle more complex cases
            pattern_key = f"{generic_name}<{','.join('T' for _ in type_args)}>"

        if pattern_key not in collections:
            # Try without generic parameters
            if generic_name in collections:
                pattern_key = generic_name
            else:
                # Fallback: rebuild generic with mapped arguments
                fallback = generic_name
                if mapped_args:
                    fallback = f"{generic_name}<{', '.join(arg.cpp_type for arg in mapped_args)}>"

                includes: list[str] = []
                for arg in mapped_args:
                    includes.extend(arg.includes)

                return TypeMappingResult(
                    cpp_type=fallback,
                    includes=sorted(set(includes)),
                    is_user_defined=True,
                )

        mapping = collections[pattern_key]
        cpp_template = mapping["cpp_type"]

        # Substitute type arguments
        if len(type_args) == 1:
            cpp_type = cpp_template.replace("{T}", mapped_args[0].cpp_type)
        elif len(type_args) == 2:
            cpp_type = cpp_template.replace("{K}", mapped_args[0].cpp_type)
            cpp_type = cpp_type.replace("{V}", mapped_args[1].cpp_type)
        else:
            # For more complex cases, do sequential substitution
            cpp_type = cpp_template
            for i, arg in enumerate(mapped_args):
                cpp_type = cpp_type.replace(f"{{T{i}}}", arg.cpp_type)

        # Combine includes from mapping and all type arguments
        includes = list(mapping.get("includes", []))
        for arg in mapped_args:
            includes.extend(arg.includes)
        includes = sorted(set(includes))

        return TypeMappingResult(
            cpp_type=cpp_type,
            includes=includes,
            comment=mapping.get("comment"),
        )

    def _map_delegate_type(
        self,
        delegate_name: str,
        type_args: List[str],
        mapped_args: List[TypeMappingResult],
    ) -> TypeMappingResult:
        """Map common C# delegate types to std::function."""

        includes: set[str] = {"<functional>"}
        for arg in mapped_args:
            includes.update(arg.includes)

        if delegate_name == "Action":
            if mapped_args:
                params = ", ".join(arg.cpp_type for arg in mapped_args)
                cpp_type = f"std::function<void({params})>"
            else:
                cpp_type = "std::function<void()>"

        elif delegate_name == "ActionRef":
            if mapped_args:
                params = ", ".join(f"{arg.cpp_type}&" for arg in mapped_args)
                cpp_type = f"std::function<void({params})>"
            else:
                cpp_type = "std::function<void()>"

        elif delegate_name == "Func":
            if not mapped_args:
                cpp_type = "std::function<void()>"
            else:
                return_type = mapped_args[-1]
                parameters = mapped_args[:-1]
                if parameters:
                    param_str = ", ".join(arg.cpp_type for arg in parameters)
                    cpp_type = f"std::function<{return_type.cpp_type}({param_str})>"
                else:
                    cpp_type = f"std::function<{return_type.cpp_type}()>"

        elif delegate_name == "FuncRef":
            if mapped_args:
                param_type = mapped_args[0].cpp_type
                cpp_type = f"std::function<{param_type}({param_type}&)>"
            else:
                cpp_type = "std::function<void()>"

        else:
            cpp_type = "std::function<void()>"

        return TypeMappingResult(
            cpp_type=cpp_type,
            includes=sorted(includes),
            comment=f"Mapped C# {delegate_name} delegate",
        )

    def _parse_type_arguments(self, type_args_str: str) -> list[str]:
        """
        Parse type arguments from a generic type string.

        Handles nested generics correctly (e.g., "Dictionary<string, List<int>>")

        Args:
            type_args_str: Type arguments string (e.g., "string, List<int>")

        Returns:
            List of type arguments
        """
        args = []
        current_arg = ""
        depth = 0

        for char in type_args_str:
            if char == "<":
                depth += 1
                current_arg += char
            elif char == ">":
                depth -= 1
                current_arg += char
            elif char == "," and depth == 0:
                args.append(current_arg.strip())
                current_arg = ""
            else:
                current_arg += char

        # Add the last argument
        if current_arg.strip():
            args.append(current_arg.strip())

        return args

    def _map_delegate_type(
        self,
        generic_name: str,
        mapped_args: list[TypeMappingResult],
    ) -> TypeMappingResult | None:
        """
        Map C# delegate types (Func<>, Action<>, etc.) to std::function equivalents.
        """
        base_name = generic_name.split(".")[-1].strip()
        base_lower = base_name.lower()

        if base_lower not in {"action", "actionref", "func", "funcref"}:
            return None

        includes: list[str] = ["<functional>"]
        for arg in mapped_args:
            includes.extend(arg.includes)

        # Deduplicate while preserving order
        seen: set[str] = set()
        deduped_includes: list[str] = []
        for header in includes:
            if header not in seen:
                deduped_includes.append(header)
                seen.add(header)

        if base_lower == "action":
            params = ", ".join(arg.cpp_type for arg in mapped_args)
            signature = f"({params})" if params else "()"
            return TypeMappingResult(
                cpp_type=f"std::function<void{signature}>",
                includes=deduped_includes,
                comment=f"Delegate {base_name} mapped to std::function",
            )

        if base_lower == "actionref":
            params = ", ".join(f"{arg.cpp_type}&" for arg in mapped_args)
            signature = f"({params})" if params else "()"
            return TypeMappingResult(
                cpp_type=f"std::function<void{signature}>",
                includes=deduped_includes,
                comment=f"Delegate {base_name} mapped to std::function with references",
            )

        if base_lower == "funcref":
            if not mapped_args:
                # Nothing to map, fall back to default handling
                return None
            return_type = mapped_args[0].cpp_type
            param_type = f"{return_type}&"
            return TypeMappingResult(
                cpp_type=f"std::function<{return_type}({param_type})>",
                includes=deduped_includes,
                comment=f"Delegate {base_name} mapped to std::function with reference parameter",
            )

        if base_lower == "func":
            if not mapped_args:
                return TypeMappingResult(
                    cpp_type="std::function<void()>",
                    includes=deduped_includes,
                    comment=f"Delegate {base_name} mapped to std::function",
                )

            return_type = mapped_args[-1].cpp_type
            param_types = [arg.cpp_type for arg in mapped_args[:-1]]
            params = ", ".join(param_types)
            signature = f"({params})" if params else "()"
            return TypeMappingResult(
                cpp_type=f"std::function<{return_type}{signature}>",
                includes=deduped_includes,
                comment=f"Delegate {base_name} mapped to std::function",
            )

        return None

    def _fix_incomplete_generics(self, type_str: str) -> str:
        """Repair malformed generic notation (e.g. missing closing brackets)."""
        if not type_str or "<" not in type_str:
            return type_str

        fixed = type_str

        open_count = fixed.count("<")
        close_count = fixed.count(">")
        if open_count > close_count:
            fixed = fixed.rstrip(", ")
            fixed += ">" * (open_count - close_count)

        fixed = re.sub(r"<\s*>", "<object>", fixed)

        def _upper_single(match: re.Match[str]) -> str:
            return f"{match.group(1)}{match.group(2).upper()}"

        fixed = re.sub(r"(<|,)\s*([a-z])(?=[,>])", _upper_single, fixed)

        return fixed

    def get_all_includes(self, types: list[str]) -> list[str]:
        """
        Get all required includes for a list of types.

        Args:
            types: List of C# type names

        Returns:
            Deduplicated list of C++ include directives
        """
        includes_set: set[str] = set()

        for type_name in types:
            result = self.map_type(type_name)
            includes_set.update(result.includes)

        return sorted(list(includes_set))
